package com.googlecode.ocl2sqlcompiler;

import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;

import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.NodeList;

import com.googlecode.ocl2sqlcompiler.components.UMLAssociation;
import com.googlecode.ocl2sqlcompiler.components.UMLAssociationClass;
import com.googlecode.ocl2sqlcompiler.components.UMLAttribute;
import com.googlecode.ocl2sqlcompiler.components.UMLClass;
import com.googlecode.ocl2sqlcompiler.components.UMLEnumeration;
import com.googlecode.ocl2sqlcompiler.components.UMLLiteral;
import com.googlecode.ocl2sqlcompiler.components.UMLLowerValue;
import com.googlecode.ocl2sqlcompiler.components.UMLOperation;
import com.googlecode.ocl2sqlcompiler.components.UMLParameter;
import com.googlecode.ocl2sqlcompiler.components.UMLParameterDirection;
import com.googlecode.ocl2sqlcompiler.components.UMLReturnResult;
import com.googlecode.ocl2sqlcompiler.components.UMLUpperValue;

public class XMIParser {

	private static final char HASH = '#';
	private static final String HREF = "href";
	private static final String RETURN_RESULT = "returnResult";
	private static final String MEMBER_END = "memberEnd";
	private static final String LOWER_VALUE = "lowerValue";
	private static final String UPPER_VALUE = "upperValue";
	private static final String OWNED_PARAMETER = "ownedParameter";
	private static final String DIRECTION = "direction";
	private static final String VALUE = "value";
	private static final String VISIBILITY = "visibility";
	private static final String ASSOCIATION = "association";
	private static final String TYPE = "type";
	private static final String OWNED_LITERAL = "ownedLiteral";
	private static final String OWNED_OPERATION = "ownedOperation";
	private static final String OWNED_ATTRIBUTE = "ownedAttribute";
	private static final String UML_ASSOCIATION_CLASS_TYPE = "uml:AssociationClass";
	private static final String UML_ENUMERATION_TYPE = "uml:Enumeration";
	private static final String UML_ASSOCIATION_TYPE = "uml:Association";
	private static final String UML_CLASS_TYPE = "uml:Class";
	private static final String OWNED_MEMBER = "ownedMember";
	private static final String ID_ATTR = "xmi:id";
	private static final String NAME_ATTR = "name";
	private static final String TYPE_ATTR = "xmi:type";

	private final Set<UMLClass> classes;
	private final List<UMLAssociation> associations;
	private final Set<UMLAssociationClass> associationClasses;
	private final Set<UMLEnumeration> enumerations;
	private final List<UMLAttribute> allAttributes;

	private final String xmiFilePath;

	private static XMIParser parser;

	public static XMIParser getInstance() {
		return parser;
	}

	public static XMIParser getInstance(String xmiFilePath) throws Exception {
		parser = new XMIParser(xmiFilePath);
		parser.init();
		return parser;
	}

	public XMIParser(String xmiFilePath) {

		this.classes = new HashSet<UMLClass>();
		this.associations = new ArrayList<UMLAssociation>();
		this.associationClasses = new HashSet<UMLAssociationClass>();
		this.enumerations = new HashSet<UMLEnumeration>();
		this.allAttributes = new ArrayList<UMLAttribute>();

		this.xmiFilePath = xmiFilePath;
	}

	public void init() throws Exception {

		final DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
		final DocumentBuilder db = dbf.newDocumentBuilder();
		final Document doc = db.parse(xmiFilePath);

		final Element elem = doc.getDocumentElement();

		// Processa o pacote, as Classes, Associa��es, enumerations, e
		// associationClasses
		NodeList nl = elem.getElementsByTagName(OWNED_MEMBER);
		for (int i = 0; i < nl.getLength(); i++) {
			Element tagMember = (Element) nl.item(i);
			processElement(tagMember);
		}

		for (int i = 0; i < nl.getLength(); i++) {
			Element tagMember = (Element) nl.item(i);
			processClassAttibutes(tagMember);
		}
	}

	private void processElement(Element element) {
		// pega os dados cadastrado para o membro atual
		final String idMember = element.getAttribute(ID_ATTR);
		final String name = element.getAttribute(NAME_ATTR);
		final String type = element.getAttribute(TYPE_ATTR);

		if (type.equals(UML_CLASS_TYPE)) {
			final UMLClass clazz = new UMLClass();
			clazz.setId(idMember);
			clazz.setName(name);
			classes.add(clazz);
		} else if (type.equals(UML_ASSOCIATION_TYPE)) {
			final UMLAssociation association = new UMLAssociation();
			association.setId(idMember);
			associations.add(association);
		} else if (type.equals(UML_ENUMERATION_TYPE)) {
			final UMLEnumeration enumeration = new UMLEnumeration();
			enumeration.setId(idMember);
			enumeration.setName(name);
			processEnumeration(element, enumeration);
		} else if (type.equals(UML_ASSOCIATION_CLASS_TYPE)) {
			// HOPE THIS NEVER OCCURS
			final UMLAssociationClass umlAssociationClass = new UMLAssociationClass();
			umlAssociationClass.setId(idMember);
			umlAssociationClass.setName(name);
			associationClasses.add(umlAssociationClass);
		}
	}

	private void processClassAttibutes(Element element) {
		String type = element.getAttribute(TYPE_ATTR);
		String idMember = element.getAttribute(ID_ATTR);

		if (type.equals(UML_CLASS_TYPE)) {
			UMLClass clazz = findClassById(idMember);
			classes.remove(clazz);

			NodeList nl = element.getElementsByTagName(OWNED_ATTRIBUTE);

			List<UMLAttribute> currentAttributes = new ArrayList<UMLAttribute>();
			for (int i = 0; i < nl.getLength(); i++) {
				Element tagMember = (Element) nl.item(i);
				UMLAttribute currentAt = processAttributes(tagMember);
				currentAttributes.add(currentAt);
				allAttributes.add(currentAt);
			}
			clazz.setAtributes(currentAttributes);

			nl = element.getElementsByTagName(OWNED_OPERATION);

			List<UMLOperation> currentOperations = new ArrayList<UMLOperation>();
			for (int i = 0; i < nl.getLength(); i++) {
				Element tagMember = (Element) nl.item(i);
				currentOperations.add(processOperations(tagMember));
			}
			clazz.setOperations(currentOperations);

			classes.add(clazz);

		} else if (type.equals(UML_ASSOCIATION_TYPE)) {
			UMLAssociation association = findAssociationById(idMember);
			associations.remove(association);

			String memberEnd = element.getAttribute(MEMBER_END);
			String[] memberEnds = memberEnd.split("\\s+");
			UMLAttribute leftAttribute = findAttributeById(memberEnds[0]);
			UMLAttribute rightAttribute = findAttributeById(memberEnds[1]);

			association.setRightAssociation(rightAttribute);
			association.setLeftAssociation(leftAttribute);

			associations.add(association);
		} else if (type.equals(UML_ASSOCIATION_CLASS_TYPE)) {
			UMLAssociationClass associationClass = findAssociationClassById(idMember);
			associationClasses.remove(associationClass);

			String memberEnd = element.getAttribute(MEMBER_END);
			String[] memberEnds = memberEnd.split("\\s+");
			UMLAttribute leftAttribute = findAttributeById(memberEnds[0]);
			UMLAttribute rightAttribute = findAttributeById(memberEnds[1]);

			associationClass.setRightAssociation(rightAttribute);
			associationClass.setLeftAssociation(leftAttribute);

			NodeList nl = element.getElementsByTagName(OWNED_ATTRIBUTE);

			List<UMLAttribute> currentAttributes = new ArrayList<UMLAttribute>();
			for (int i = 0; i < nl.getLength(); i++) {
				Element tagMember = (Element) nl.item(i);
				UMLAttribute currentAt = processAttributes(tagMember);
				currentAttributes.add(currentAt);
				allAttributes.add(currentAt);
			}
			associationClass.setAttributes(currentAttributes);

			associationClasses.add(associationClass);
		}

	}

	private UMLAttribute processAttributes(Element element) {
		UMLAttribute attribute = new UMLAttribute();
		String classId = element.getAttribute(TYPE);
		UMLClass currentClass = findClassById(classId);

		String associationId = element.getAttribute(ASSOCIATION);
		UMLAssociation associationClass = findAssociationById(associationId);
		String visibility = element.getAttribute(VISIBILITY);

		attribute.setName(element.getAttribute(NAME_ATTR));
		attribute.setId(element.getAttribute(ID_ATTR));
		if (currentClass != null) {
			attribute.setType(currentClass);
		} else {
			NodeList nl = element.getElementsByTagName(TYPE);
			if (((Element) nl.item(0)) != null) {
				String type = ((Element) nl.item(0)).getAttribute(HREF);
				type = type.substring(type.indexOf(HASH) + 1);
				UMLClass attributeType = new UMLClass();
				attributeType.setName(type);
				attribute.setType(attributeType);
			}
		}
		attribute.setAssociation(associationClass);
		if (visibility.length() > 0)
			attribute.setVisibility(visibility);

		// seta upper e lower values
		NodeList nl = element.getElementsByTagName(UPPER_VALUE);
		UMLUpperValue upperValue = new UMLUpperValue();
		if (((Element) nl.item(0)) != null) {
			upperValue.setType(((Element) nl.item(0)).getAttribute(TYPE_ATTR));
			upperValue.setId(((Element) nl.item(0)).getAttribute(ID_ATTR));
			upperValue.setValue(Integer.parseInt(((Element) nl.item(0))
					.getAttribute(VALUE)));
			attribute.setUpperValue(upperValue);
		}

		nl = element.getElementsByTagName(LOWER_VALUE);
		UMLLowerValue lowerValue = new UMLLowerValue();
		if (((Element) nl.item(0)) != null) {
			lowerValue.setType(((Element) nl.item(0)).getAttribute(TYPE_ATTR));
			lowerValue.setId(((Element) nl.item(0)).getAttribute(ID_ATTR));
			if (((Element) nl.item(0)).getAttribute(VALUE).length() > 0)
				lowerValue.setValue(Integer.parseInt(((Element) nl.item(0))
						.getAttribute(VALUE)));
			attribute.setLowerValue(lowerValue);
		}

		return attribute;
	}

	private UMLOperation processOperations(Element element) {
		UMLOperation operation = new UMLOperation();
		operation.setId(element.getAttribute(ID_ATTR));
		operation.setName(element.getAttribute(NAME_ATTR));
		String classId = element.getAttribute(TYPE);
		UMLClass currentClass = findClassById(classId);

		// seta o tipo
		if (currentClass != null)
			operation.setType(currentClass);
		else {
			NodeList nl = element.getElementsByTagName(TYPE);
			if (nl.getLength() > 0) {
				String type = ((Element) nl.item(0)).getAttribute(HREF);
				type = type.substring(type.indexOf(HASH) + 1);
				UMLClass operationType = new UMLClass();
				operationType.setName(type);
				operation.setType(operationType);
			}
		}

		// seta os parametros
		List<UMLParameter> operationParameters = new ArrayList<UMLParameter>();
		NodeList nl = element.getElementsByTagName(OWNED_PARAMETER);
		for (int i = 0; i < nl.getLength(); i++) {
			UMLParameter parameter = new UMLParameter();
			parameter.setId(((Element) nl.item(i)).getAttribute(ID_ATTR));
			parameter.setName(((Element) nl.item(i)).getAttribute(NAME_ATTR));
			parameter.setDirection(UMLParameterDirection.valueOf(((Element) nl
					.item(i)).getAttribute(DIRECTION).toUpperCase()));

			String parameterType = ((Element) nl.item(i)).getAttribute(TYPE);
			UMLClass parameterTypeClass = findClassById(parameterType);

			// seta o tipo do parametro
			if (parameterTypeClass != null)
				parameter.setType(parameterTypeClass);
			else {
				NodeList pType = ((Element) nl.item(i))
						.getElementsByTagName(TYPE);
				if (pType.getLength() > 0) {
					String type = ((Element) pType.item(0))
							.getAttribute(TYPE_ATTR);
					UMLClass operationType = new UMLClass();
					operationType.setName(type);
					parameter.setType(operationType);
				}
			}

			// seta o returnResult
			UMLReturnResult returnResult = new UMLReturnResult();

			NodeList rResult = ((Element) nl.item(i))
					.getElementsByTagName(RETURN_RESULT);
			if (rResult.getLength() > 0) {
				String id = ((Element) rResult.item(0)).getAttribute(ID_ATTR);

				returnResult.setId(id);

				String rRtype = ((Element) rResult.item(0)).getAttribute(TYPE);
				UMLClass rRClass = findClassById(rRtype);

				if (rRClass != null)
					returnResult.setType(rRClass);
				else {
					NodeList rrType = ((Element) rResult.item(0))
							.getElementsByTagName(TYPE);
					if (rrType.getLength() > 0) {
						String type = ((Element) rrType.item(0))
								.getAttribute(TYPE_ATTR);
						UMLClass operationType = new UMLClass();
						operationType.setName(type);
						returnResult.setType(operationType);
					}
				}
				operation.setReturnResult(returnResult);
			}

			operationParameters.add(parameter);
		}

		operation.setParameters(operationParameters);
		return operation;

	}

	private void processEnumeration(Element element, UMLEnumeration enumeration) {

		List<UMLLiteral> literals = new ArrayList<UMLLiteral>();
		NodeList nl = element.getElementsByTagName(OWNED_LITERAL);
		for (int i = 0; i < nl.getLength(); i++) {
			Element tagMember = (Element) nl.item(i);
			UMLLiteral newLiteral = new UMLLiteral();
			newLiteral.setId(tagMember.getAttribute(ID_ATTR));
			newLiteral.setName(tagMember.getAttribute(NAME_ATTR));
			literals.add(newLiteral);
		}

		enumeration.setLiterals(literals);

		enumerations.add(enumeration);
	}

	private OCLType getType(String primitiveCode) {
		if (primitiveCode.equals("_IXlH8a86EdieaYgxtVWN8Q"))
			return OCLType.STRING;
		if (primitiveCode.equals("_IXfBUK86EdieaYgxtVWN8Q"))
			return OCLType.BOOLEAN;
		if (primitiveCode.equals("_IXlH8K86EdieaYgxtVWN8Q"))
			return OCLType.INTEGER;
		return null;
	}

	private UMLClass findClassById(String id) {
		for (UMLClass clazz : classes) {
			if (clazz.getId().equals(id)) {
				return clazz;
			}
		}
		return null;
	}

	private UMLAssociation findAssociationById(String id) {
		for (UMLAssociation assoc : associations) {
			if (assoc.getId().equals(id)) {
				return assoc;
			}
		}
		return null;
	}

	private UMLAttribute findAttributeById(String id) {
		for (UMLAttribute attr : allAttributes) {
			if (attr.getId().equals(id)) {
				return attr;
			}
		}
		return null;
	}

	private UMLAssociationClass findAssociationClassById(String id) {
		for (UMLAssociationClass clazz : associationClasses) {
			if (clazz.getId().equals(id)) {
				return clazz;
			}
		}
		return null;
	}

	public String getType(String currentObject, String target) {
		for (UMLClass clazz : classes) {
			if (clazz.getName().equals(currentObject)) {
				for (int j = 0; j < clazz.getAtributes().size(); j++) {
					UMLAttribute currentAttribute = clazz.getAtributes().get(j);
					if (currentAttribute.getName().equals(target))
						return currentAttribute.getType().getName();
				}

				for (int j = 0; j < clazz.getOperations().size(); j++) {
					UMLOperation currentOperation = clazz.getOperations()
							.get(j);
					if (currentOperation.getName().equals(target))
						return currentOperation.getType().getName();
				}
			}
		}
		return null;
	}

	public OCLSemanticObject<String> getOCLSemanticObject(String currentObject,
			String target) {
		for (UMLClass clazz : classes) {
			if (clazz.getName().equals(currentObject)) {
				for (int j = 0; j < clazz.getAtributes().size(); j++) {
					UMLAttribute currentAttribute = clazz.getAtributes().get(j);
					if (currentAttribute.getName().equals(target)) {
						String currentType = currentAttribute.getType()
								.getName();
						OCLType primitiveType = getType(currentType);
						if (primitiveType != null) {
							return new OCLSemanticObject<String>(primitiveType,
									target);
						} else {
							return new OCLSemanticObject<String>(OCLType.MODEL,
									currentType);
						}
					}

				}

				for (UMLOperation currentOperation : clazz.getOperations()) {
					String currentType = currentOperation.getType().getName();
					OCLType primitiveType = getType(currentType);
					if (primitiveType != null)
						return new OCLSemanticObject<String>(primitiveType,
								target);
					else {
						return new OCLSemanticObject<String>(OCLType.MODEL,
								currentType);
					}
				}
			}
		}
		return null;
	}

	public boolean existsType(String type) {
		for (UMLClass clazz : classes) {
			if (clazz.getName().equals(type)) {
				return true;
			}
		}
		return false;
	}

	/**
	 * Verifica se existe algum relacionamento entre duas classes
	 * 
	 * @param class1
	 *            o nome da primeira classe
	 * @param class2
	 *            o nome da segunda classe
	 * @return true, se houver relacionamento entre elas; false, caso contrario
	 */
	public boolean areRelated(String class1, String class2) {
		for (int i = 0; i < associations.size(); i++) {
			if (associations.get(i).getLeftAssociation().getName()
					.equals(class1))
				if (associations.get(i).getRightAssociation().getName()
						.equals(class2))
					return true;
		}
		return false;
	}

}